package top.v5it.japi.plus.extra.web;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.support.AopUtils;
import org.springframework.web.bind.annotation.*;
import top.v5it.japi.plus.core.spring.web.JapiEada;
import top.v5it.japi.plus.extra.entity.Entity;
import top.v5it.japi.plus.extra.exception.JapiBatchException;
import top.v5it.japi.plus.extra.exception.JapiMethodNotFoundException;
import top.v5it.japi.plus.extra.exception.JapiOrmException;
import top.v5it.japi.plus.extra.util.EntityUtils;
import top.v5it.japi.plus.log.JapiOperLog;
import top.v5it.japi.plus.support.limit.JapiLimit;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

/**
 * 统一批量操作控制器，可支持 POST 或者 GET 方法
 *
 * <pre>
 * ### 示例一POST：一个集合参数
 * POST http://localhost:7474/batch/account/addAllInBatch
 * Content-Type: application/json
 * Accept: application/json
 *
 * [
 *   {
 *     "name": "adb",
 *     "amount": 10000
 *   },
 *   {
 *     "name": "ddb",
 *     "amount": 10000
 *   }
 * ]
 *
 * ### 示例二POST：第一个参数为集合参数，第二个参数为常量，其他参数按依次通过url传参
 * POST http://localhost:7474/batch/account/addAllInBatch?batchSize=100
 * Content-Type: application/json
 * Accept: application/json
 *
 * [
 *   {
 *     "name": "adb",
 *     "amount": 10000
 *   },
 *   {
 *     "name": "ddb",
 *     "amount": 10000
 *   }
 * ]
 * </pre>
 *
 * @author zhanpu
 * @date 2022/8/10
 */
@Slf4j
@RestController
public class JapiBatchController extends JapiBaseController {

    private static final String BATCH_URL = "batch/{bean}/{method}";

    @JapiEada
    @JapiOperLog("批量操作")
    @JapiLimit(limitKey = BATCH_URL)
    @RequestMapping(value = BATCH_URL, method = {RequestMethod.GET, RequestMethod.POST})
    public Object execute(@PathVariable("bean") String bean, @PathVariable("method") String methodName,
                          @RequestBody List<HashMap<String, Object>> jsons, HttpServletRequest request) {
        Object service;
        try {
            service = getBean(bean);
        } catch (RuntimeException e) {
            throw new JapiOrmException(e);
        }
        Method method = getMethod(service, bean, methodName);
        Map<String, Object> json = toMap(null, request);
        final Class<?> targetClass = service.getClass();
        final Method mostSpecificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
        Object vo = getValueObj(mostSpecificMethod, method, jsons);
        Object[] args = getArguments(mostSpecificMethod, json, vo);
        return invoke(service, method, args);
    }

    /**
     * get the first parameter of the method, as the value object, and instance, set values from the json.
     * if the json is null or empty, means no value object, then return null.
     * if there is no parameters of the method, then return null.
     *
     * @param method
     * @param jsons
     * @return the value object with it's data.
     */
    @SuppressWarnings({"rawtypes"})
    private Object getValueObj(Method srcMethod, Method method, List<HashMap<String, Object>> jsons) {
        if (CollUtil.isEmpty(jsons)) throw new IllegalArgumentException("请求参数不能为空");
        Class<?>[] allOfParameterTypes = method.getParameterTypes();
        if (allOfParameterTypes.length == 0) throw new JapiMethodNotFoundException("请求方法错误");
        // 第一参数必须为集合
        Class<?> firstOfParameterType = allOfParameterTypes[0];
        if (Iterable.class.isAssignableFrom(firstOfParameterType)) {
            // 获取集合中泛型类型来确定数据类型，并转换数据
            Type[] genericParameterTypes = srcMethod.getGenericParameterTypes();
            Class<?> clazz = Object.class;
            try {
                final ParameterizedType parameterizedType = (ParameterizedType) genericParameterTypes[0];
                final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                clazz = (Class<?>) ((ParameterizedType) actualTypeArguments[0]).getRawType();
            } catch (ClassCastException e) {
                // 方法中泛型类型始终都是object，通过获取类上泛型类型来确定方法参数类型
                final Class<?> superclass = method.getDeclaringClass().getSuperclass();
                if (Objects.isNull(superclass)) return jsons;
                final ParameterizedType classParameterizedType = (ParameterizedType) superclass.getGenericSuperclass();
                if (Objects.isNull(classParameterizedType)) return jsons;
                final Type[] classActualTypeArguments = classParameterizedType.getActualTypeArguments();
                for (Type actualTypeArgument : classActualTypeArguments) {
                    try {
                        clazz = Class.forName(actualTypeArgument.getTypeName());
                        if (EntityUtils.isEntity(clazz)) break;
                    } catch (ClassNotFoundException ex) {
                        // ignore
                        log.error(ex.getMessage(), ex);
                    }
                }
            }
            if (Map.class.isAssignableFrom(clazz)) return jsons;
            if (EntityUtils.isEntity(clazz)) {
                List<Entity> entities = new ArrayList<>();
                for (Map<String, Object> json : jsons) {
                    Entity entity = createEntityAndValidate(clazz, json);
                    entities.add(entity);
                }
                return entities;
            }
        }
        throw new IllegalArgumentException("请求参数不符");
    }

    /**
     * 创建entity并且验证entity字段合法性
     *
     * @param clazz
     * @param json
     * @return
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private Entity createEntityAndValidate(Class<?> clazz, Map<String, Object> json) {
        final Entity entity = EntityUtils.createEntity((Class<Entity>) clazz, json);
        validate(entity);
        return entity;
    }

    /**
     * invoke the method of the service using reflect.
     * if the value object is available, it must be the first argument.
     *
     * @param service the service
     * @param method  the method
     * @param args    the other arguments
     * @return the result after invoking.
     */
    private Object invoke(Object service, Method method, Object[] args) {
        try {
            return ArrayUtil.isEmpty(args)
                    ? method.invoke(service)
                    : method.invoke(service, args);
        } catch (IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            throw new JapiBatchException("error when invoking the service by reflect", e);
        }
    }
}
